# Sketch - A Python-based interactive drawing program
# Copyright (C) 1997, 1998, 1999, 2000, 2003 by Bernhard Herzog
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

#
#	Font management...
#

import os, re, operator
from string import split, strip, atoi, atof, lower, translate, maketrans


import streamfilter

from Sketch import _, config, const, Point, TrafoType, Scale, SketchError, \
     SketchInternalError, Subscribe, CreatePath, CreateFontMetric, SKCache
from Sketch.Lib import encoding

from Sketch.warn import warn, INTERNAL, USER, pdebug
from Sketch.Lib.util import find_in_path, find_files_in_path

minus_tilde = maketrans('-', '~')

def xlfd_matrix(trafo):
    mat = '[%f %f %f %f]' % trafo.matrix()
    return translate(mat, minus_tilde)


def _str(val):
    return strip(val)

def _bb(val):
    return tuple(map(int, map(round, map(atof, split(strip(val))))))

def _number(val):
    return int(round(atof(val)))

converters = {
    'EncodingScheme':	_str,
    'Ascender':		_number,
    'Descender':	_number,
    'ItalicAngle':	atof,
    'FontBBox':		_bb,
    'StartCharMetrics':	None,
    'EndFontMetrics':	None
}

StandardEncoding = 'AdobeStandardEncoding'

def read_char_metrics(afm):
    # read the individual char metrics. Assumes that each line contains
    # the keys C, W, N and B. Of these keys, only C (or CH, but that's
    # not implemented here) is really required by the AFM specification.
    charmetrics = {encoding.notdef: (0, 0,0,0,0)}
    font_encoding = [encoding.notdef] * 256
    while 1:
        line = afm.readline()
        if line == 'EndCharMetrics\n':
            break
        items = filter(None, map(strip, split(line, ';')))
        if not items:
            continue
        code = name = width = bbox = None
        for item in items:
            [key, value] = split(item, None, 1)
            if key == 'C':
                code = atoi(value)
            elif key == 'WX':
                width = int(round(atof(value)))
            elif key == 'N':
                name = value
            elif key == 'B':
                bbox = tuple(map(int,map(round,map(atof,split(value)))))
        charmetrics[name] = (width,) + bbox
        font_encoding[code] = name
    return charmetrics, font_encoding

def read_afm_file(filename):
    afm = streamfilter.LineDecode(open(filename, 'r'))

    attribs = {'ItalicAngle': 0.0}
    charmetrics = None
    font_encoding = [encoding.notdef] * 256

    while 1:
        line = afm.readline()
        if not line:
            break
        try:
            [key, value] = split(line, None, 1)
        except ValueError:
            # this normally means that a line contained only a keyword
            # but no value or that the line was empty
            continue
        try:
            action = converters[key]
        except KeyError:
            continue
        if action:
            attribs[key] = action(value)
        elif key == 'StartCharMetrics':
            charmetrics, font_encoding = read_char_metrics(afm)
            break
        else:
            # EndFontMetrics
            break

    if not charmetrics:
        raise ValueError, \
              'AFM files without individual char metrics not yet supported.'

    if attribs.get('EncodingScheme', StandardEncoding) == StandardEncoding:
        enc = encoding.iso_latin_1
    else:
        enc = font_encoding

    try:
        rescharmetrics = map(operator.getitem, [charmetrics] * len(enc), enc)
    except KeyError:
        # Some iso-latin-1 glyphs are not defined in the font. Try the
        # slower way and report missing glyphs.
        length = len(enc)
        rescharmetrics = [(0, 0,0,0,0)] * length
        for idx in range(length):
            name = enc[idx]
            try:
                rescharmetrics[idx] = charmetrics[name]
            except KeyError:
                # missing character...
                warn(INTERNAL, '%s: missing character %s', filename, name)

    # some fonts don't define ascender and descender (psyr.afm for
    # instance). use the values from the font bounding box instead. This
    # is not really a good idea, but how do we solve this?
    #
    # If psyr.afm is the only afm-file where these values are missing
    # (?) we could use the values from the file s050000l.afm shipped
    # with ghostscript (or replace psyr.afm with that file).
    #
    # This is a more general problem since many of the values Sketch
    # reads from afm files are only optional (including ascender and
    # descender).
    if not attribs.has_key('Ascender'):
        attribs['Ascender'] = attribs['FontBBox'][3]
    if not attribs.has_key('Descender'):
        attribs['Descender'] = attribs['FontBBox'][1]

    return (CreateFontMetric(attribs['Ascender'], attribs['Descender'],
                             attribs['FontBBox'], attribs['ItalicAngle'],
                             rescharmetrics),
            enc)


_warned_about_afm = {}
def read_metric(ps_name):
    for afm in ps_to_filename[ps_name]:
        afm = afm + '.afm'
        filename = find_in_path(config.font_path, afm)
        if filename:
            if __debug__:
                import time
                start = time.clock()
            metric = read_afm_file(filename)
            if __debug__:
                pdebug('timing', 'time to read afm %s: %g', afm,
                       time.clock() - start)
            return metric
    else:
        if not _warned_about_afm.get(afm):
            warn(USER,
                 _("I cannot find the metrics for the font %(ps_name)s.\n"
                   "The file %(afm)s is not in the font_path.\n"
                   "I'll use the metrics for %(fallback)s instead."),
                 ps_name = ps_name, afm = afm,
                 fallback = config.preferences.fallback_font)
            _warned_about_afm[afm] = 1
        if ps_name != config.preferences.fallback_font:
            return read_metric(config.preferences.fallback_font)
        else:
            raise SketchError("Can't load metrics for fallback font %s",
                              config.preferences.fallback_font)


def font_file_name(ps_name):
    names = []
    for basename in ps_to_filename[ps_name]:
        names.append(basename + '.pfb')
        names.append(basename + '.pfa')
    filename = find_files_in_path(config.font_path, names)
    return filename


def read_outlines(ps_name):
    filename = font_file_name(ps_name)
    if filename:
        if __debug__:
            pdebug('font', 'read_outlines: %s', filename)

        import Sketch.Lib.type1
        return Sketch.Lib.type1.read_outlines(filename)
    else:
        raise SketchInternalError('Cannot find file for font %s' % ps_name)

def convert_outline(outline):
    paths = []
    trafo = Scale(0.001)
    for closed, sub in outline:
        if closed:
            sub.append(sub[0])
        path = CreatePath()
        paths.append(path)
        for item in sub:
            if len(item) == 2:
                apply(path.AppendLine, item)
            else:
                apply(path.AppendBezier, item)
        if closed:
            path.load_close()
    for path in paths:
        path.Transform(trafo)
    return tuple(paths)




fontlist = []
fontmap = {}
ps_to_filename = {}

def _add_ps_filename(ps_name, filename):
    filename = (filename,)
    if ps_to_filename.has_key(ps_name):
        filename = ps_to_filename[ps_name] + filename
    ps_to_filename[ps_name] = filename

def read_font_dirs():
    #print 'read_font_dirs'
    if __debug__:
        import time
        start = time.clock()

    rx_sfd = re.compile(r'^.*\.sfd$')
    for directory in config.font_path:
        #print directory
        try:
            filenames = os.listdir(directory)
        except os.error, exc:
            warn(USER, _("Cannot list directory %s:%s\n"
                         "ignoring it in font_path"),
                 directory, str(exc))
            continue
        dirfiles = filter(rx_sfd.match, filenames)
        for filename in dirfiles:
            filename = os.path.join(directory, filename)
            #print filename
            try:
                file = open(filename, 'r')
                line_nr = 0
                for line in file.readlines():
                    line_nr = line_nr + 1
                    line = strip(line)
                    if not line or line[0] == '#':
                        continue
                    info = map(intern, split(line, ','))
                    if len(info) == 6:
                        psname = info[0]
                        fontlist.append(tuple(info[:-1]))
                        _add_ps_filename(psname, info[-1])
                        fontmap[psname] = tuple(info[1:-1])
                    elif len(info) == 2:
                        psname, basename = info
                        _add_ps_filename(psname, basename)
                    else:
                        warn(INTERNAL,'%s:%d: line must have exactly 6 fields',
                             filename, line_nr)
                file.close()
            except IOError, value:
                warn(USER, _("Cannot load sfd file %(filename)s:%(message)s;"
                             "ignoring it"),
                     filename = filename, message = value.strerror)
    if __debug__:
        pdebug('timing', 'time to read font dirs: %g', time.clock() - start)

def make_family_to_fonts():
    families = {}
    for item in fontlist:
        family = item[1]
        fontname = item[0]
        if families.has_key(family):
            families[family] = families[family] + (fontname,)
        else:
            families[family] = (fontname,)
    return families



xlfd_template = "%s--%s-*-*-*-*-*-%s"

font_cache = SKCache()

class Font:

    def __init__(self, name):
        self.name = name
        info = fontmap[name]
        family, font_attrs, xlfd_start, encoding_name = info
        self.family = family
        self.font_attrs = font_attrs
        self.xlfd_start = lower(xlfd_start)
        self.encoding_name = encoding_name
        self.metric, self.encoding = read_metric(self.PostScriptName())
        self.outlines = None

        self.ref_count = 0
        font_cache[self.name] = self

    def __del__(self):
        if font_cache is not None and font_cache.has_key(self.name):
            del font_cache[self.name]

    def __repr__(self):
        return "<Font %s>" % self.name

    def GetXLFD(self, size_trafo):
        if type(size_trafo) == TrafoType:
            if size_trafo.m11 == size_trafo.m22 > 0\
               and size_trafo.m12 == size_trafo.m21 == 0:
                # a uniform scaling. Special case for better X11R5
                # compatibility
                return xlfd_template % (self.xlfd_start,
                                        int(round(size_trafo.m11)),
                                        self.encoding_name)
            return xlfd_template % (self.xlfd_start, xlfd_matrix(size_trafo),
                                    self.encoding_name)
        return xlfd_template % (self.xlfd_start, int(round(size_trafo)),
                                self.encoding_name)

    def PostScriptName(self):
        return self.name

    def TextBoundingBox(self, text, size):
        # Return the bounding rectangle of TEXT when set in this font
        # with a size of SIZE. The coordinates of the rectangle are
        # relative to the origin of the first character.
        llx, lly, urx, ury = self.metric.string_bbox(text)
        size = size / 1000.0
        return (llx * size, lly * size, urx * size, ury * size)

    def TextCoordBox(self, text, size):
        # Return the coord rectangle of TEXT when set in this font with
        # a size of SIZE. The coordinates of the rectangle are relative
        # to the origin of the first character.
        metric = self.metric
        width = metric.string_width(text)
        size = size / 1000.0
        return (0,		metric.descender * size,
                width * size,	metric.ascender * size)

    def TextCaretData(self, text, pos, size):
        from math import tan, pi
        size = size / 1000.0
        x = self.metric.string_width(text, pos) * size
        lly = self.metric.lly * size
        ury = self.metric.ury * size
        t = tan(self.metric.italic_angle * pi / 180.0);
        up = ury - lly
        return Point(x - t * lly, lly), Point(-t * up, up)

    def TypesetText(self, text):
        return self.metric.typeset_string(text)

    def IsPrintable(self, char):
        return self.encoding[ord(char)] != encoding.notdef

    def GetOutline(self, char):
        if self.outlines is None:
            self.char_strings, self.cs_interp \
                = read_outlines(self.PostScriptName())
            self.outlines = {}
        char_name = self.encoding[ord(char)]
        outline = self.outlines.get(char_name)
        if outline is None:
            self.cs_interp.execute(self.char_strings[char_name])
            outline = convert_outline(self.cs_interp.paths)
            self.outlines[char_name] = outline
            self.cs_interp.reset()
        copy = []
        for path in outline:
            path = path.Duplicate()
            copy.append(path)
        return tuple(copy)

    def FontFileName(self):
        return font_file_name(self.PostScriptName())



_warned_about_font = {}
def GetFont(fontname):
    if font_cache.has_key(fontname):
        return font_cache[fontname]
    if not fontmap.has_key(fontname):
        if not _warned_about_font.get(fontname):
            warn(USER, _("I can't find font %(fontname)s. "
                         "I'll use %(fallback)s instead"),
                 fontname = fontname,
                 fallback = config.preferences.fallback_font)
            _warned_about_font[fontname] = 1
        if fontname != config.preferences.fallback_font:
            return GetFont(config.preferences.fallback_font)
        raise ValueError, 'Cannot find font %s.' % fontname
    return Font(fontname)


#
#	Initialisation on import
#

Subscribe(const.INITIALIZE, read_font_dirs)
